package FileWorks;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.LinkedList;

/** <b>Класс файловой структуры данных</b><br>
 * Может хранить любые классы данных, реализующие интерфейс <code>SelfFileReadAndWrite</code><br>
 * По внешнему виду очень смахивает на хэшированный файл,
 * хотя какой-либо связи с процессом хеширования не имеет.<br>
 * Вообще предназначем для хранения любого количества данных, связанных
 * с определенным индексом. Количество этих индексов задается при создании.
 * Данные хранятся в отдельном файле и разбиты на блоки, связанные в файловые
 * списки.<br>
 * Реализованы только методы, необходимые для выполнения
 * <u>ТЗ 1008. Организация процесса хранения и доставки пользовательских сообщений</u>,
 * возможна дальнейшая эволюция в полноценную файловую структуру данных
 *
 * @author Караваев Кирилл, ID 1004
 * @version 0.1 Базовая функциональность
 */
public class FileStructure<E extends SelfFileReadAndWrite>
{
    private final int INTSIZE = Integer.SIZE/8;
    private final int LONGSIZE = Long.SIZE/8;
    /** Адрес смещения начала описания индексов в главном файле */
    private int FPOFF = 0;
    
    //For main file
    int N, M;				//Количество индексов и размер блока
    long[] FilePointers = null;		//Указатели на блоки для индексов
    int IndexesAmount = 0;		//количество задействованных индексов
    RandomAccessFile MainFile = null;	//Управление главным файлом
    File MF;
    
    //For data file
    RandomAccessFile DataFile = null;	//Управление файлом данных
    File DF;
    
//========ПРИВАТНЫЕ МЕТОДЫ=====================================================
    /** Создать новый главный файл описания структуры данных
     * @param MFName путь и имя создаваемого файла
     * @param DFName путь и имя файла, в котором будут располагаться непосредственно данные
     * @param Rows количество индексов, которые может хранить структура
     * @param BlockSz размер блоков файловых ссылок, закрепленных за каждым индексом
     * @throws IOException если не удается по каким-то причинам работать с файлом
     * @throws IllegalArgumentException если заданы расово неверные параметры
     */
    private void createMainFile(String MFName, String DFName, int Rows, int BlockSz)
	    throws FileNotFoundException, IOException, IllegalArgumentException
    {
	//Проверка правильности параметров
	if (MFName == null || DFName == null || Rows <= 0 || BlockSz <= 0)
	    throw new IllegalArgumentException(this.toString() + " error in createMainFile");
	//Первичные инициализации
	N = Rows; M = BlockSz;
	FilePointers = new long[N];
	MF = new File(MFName);
	if (MF.exists()) MF.delete();	//Если файл уже есть - удаляем
	else MF.createNewFile();	//Иначе создаем
	
	MainFile = new RandomAccessFile(MF, "rw");
	int DFNLen = DFName.length();	//Это размер строки с именем файла данных
	//Пытаемся записать основные данные в файл
	try
	{
	    MainFile.writeInt(N);	//Количество индексов
	    MainFile.writeInt(M);	//Размерность блока
	    MainFile.writeInt(DFNLen);	//Размер строки с именем файла данных
	    MainFile.write(DFName.getBytes());	//Сама строка с именем файла данных
	    //Выделяем место под файловые указатели
	    FPOFF = 3*INTSIZE + DFNLen;
	    MainFile.seek(FPOFF + (N)*LONGSIZE);
	    MainFile.writeInt(0x11111111);  //Разделитель, для удобства восприятия
	} catch (IOException e)
	//Если запись не удалась - сносим все к чертям
	{
	    N = M = 0; FilePointers = null;
	    MainFile.close(); MainFile = null;
	    MF.delete(); MF = null;
	    
	    DataFile.close(); DataFile = null;
	    DF = null;
	    IndexesAmount = 0;
	    
	    throw e;	//И передаем исключение дальше
	}
    }
    /** Создать новый файл для хранения данных
     * @param DataFile имя и путь к создаваемому файлу
     * @throws IOException если невозможно по каким-либо причинам работать с файлом
     * @throws IllegalArgumentException если заданы расово неверные параметры
     * @throws IllegalStateException если главный файл не был создан ранее
     */
    private void createDataFile(String DFileName)
	    throws IOException, IllegalArgumentException, IllegalStateException
    {
	//Сначала должен быть создан Main File
	if (MainFile == null)
	    throw new IllegalStateException("Must be created Main File first");
	//Проверка правильности параметров
	if (DFileName == null)
	    throw new IllegalArgumentException(this.toString() + " error in createDataFile");
	
	DF = new File(DFileName);
	if (DF.exists()) DF.delete();	//Если файл существует - удаляем
	else DF.createNewFile();	//иначе создаем
	DataFile = new RandomAccessFile(DF, "rw");
	//Первые INTSIZE байт заняты, чтобы не допустить ссылок на нулевое смещение в файле
	try
	{
	    DataFile.writeInt(-1);
	} catch (IOException e)
	//Если запись не удалась - сносим все к чертям
	{
	    N = M = 0; FilePointers = null;
	    MainFile.close(); MainFile = null;
	    MF.delete(); MF = null;
	    
	    DataFile.close(); DataFile = null;
	    DF = null;
	    
	    throw e;	//И передаем исключение дальше
	}
    }
    
//========ПУБЛИЧНЫЕ МЕТОДЫ=====================================================
    public FileStructure() {}
    
    /** Создание полной структуры данных в файлах
     * @param MainFName путь и имя создаваемого файла
     * @param DataFName путь и имя файла, в котором будут располагаться непосредственно данные
     * @param RowsAmount количество индексов, которые может хранить структура
     * @param BlockSize размер блоков файловых ссылок, закрепленных за каждым индексом
     * @throws IOException если не удается по каким-то причинам работать с файлом
     * @throws IllegalArgumentException если заданы расово неверные параметры
     */
    public void createDataStorage(String MainFName, String DataFName, int RowsAmount, int BlockSize)
	    throws IOException, IllegalArgumentException
    {
	createMainFile(MainFName, DataFName, RowsAmount, BlockSize);
	createDataFile(DataFName);
    }
    
    /** Добавление данных к указанному индексу
     * @param Index нужный индекс
     * @param Data добавляемые данные
     * @throws IllegalArgumentException неверное задан индекс
     * @throws IllegalStateException файловая структура не была создана ранее
     * @return результат выполнения
     */
    public boolean addFor(int Index, E Data)
	    throws IllegalArgumentException, IllegalStateException
    {
	//Проверки корректности проведения операции
	if (MainFile == null || DataFile == null)
	    throw new IllegalStateException("Files wont be created");
	if (Index >= N || Index < 0)
	    throw new IllegalArgumentException(this.toString() + " error in set");
	
	//Первым делом пытаемся записать новые данные в файл данных
	long DataRef;
	try
	{ 
	    DataRef = DataFile.length();	//смещение новых данных в файле данных
	    DataFile.seek(DataRef);
	    Data.writeToFile(DataFile);
	} catch (IOException e)
	{
	    //Вот ведь отстой!
	    return false;
	}
	//Теперь возможно два варианта
	if (FilePointers[Index] == 0)	//Опа, за этим индексом не закреплено ни одного блока
	{
	    try
	    {
		long BlockRef = MainFile.length();    //Смещение начала группы блоков в главном файле
		//Создаем группу блоков
		MainFile.seek(BlockRef);	//Первые LONGSIZE байт содержат ссылку на последний блок
		MainFile.writeLong(BlockRef + LONGSIZE);	//Сейчас это текущий блок
		//Создаем блок
		MainFile.writeLong(1);	//Первый long блока - количество элементов в нем
		MainFile.writeLong(DataRef);	//И первой ссылкой становится адрес новых данных
		MainFile.seek(BlockRef + 2*LONGSIZE + M*LONGSIZE);
		MainFile.writeLong(-1);	//Конец оформления блока - адрес следующего
		//Сохраняем все новые данные
		MainFile.seek(FPOFF + Index*LONGSIZE);
		MainFile.writeLong(BlockRef);
		FilePointers[Index] = BlockRef;
	    } catch (IOException e)
	    {
		return false;
	    }
	    IndexesAmount++;
	} else //Блоки есть, нужно дописать новые данные
	{
	    try
	    {
		MainFile.seek(FilePointers[Index]);
		long LastBlock = MainFile.readLong();	//Читаем адрес последнего блока
		MainFile.seek(LastBlock);
		long BlCap = MainFile.readLong();	//Читаем размер блока
		if (BlCap == M)	//Если блок полон - создать новый
		{
		    long NewBlock = MainFile.length();
		    MainFile.seek(NewBlock);
		    MainFile.writeLong(1);
		    MainFile.writeLong(DataRef);
		    MainFile.seek(NewBlock + LONGSIZE + M*LONGSIZE);
		    MainFile.writeLong(-1);
		    
		    MainFile.seek(LastBlock + LONGSIZE + M*LONGSIZE);
		    MainFile.writeLong(NewBlock);
		    MainFile.seek(FilePointers[Index]);
		    MainFile.writeLong(NewBlock);
		}
		else	//иначе добавить в последний 
		{
		    MainFile.seek(LastBlock);
		    BlCap++;
		    MainFile.writeLong(BlCap);
		    MainFile.seek(LastBlock + LONGSIZE + (BlCap - 1)*LONGSIZE);
		    MainFile.writeLong(DataRef);
		}
	    } catch (IOException e)
	    {
		return false;
	    }
	}
	return true;
    }
    
    /** Запрос на наличие данных для указанного индекса
     * @param Index нужный индекс
     * @return результат проверки - есть или нет
     */
    public boolean existsDataFor(int Index)
    {
	if (FilePointers == null) return false;
	return (FilePointers[Index] == 0 ? false : true);
    }
    
    /** Изъять все данные для указанного индекса
     * @param Index нужный индекс
     * @param temp рабочий экземпляр класса хранимых данных, чтобы делать новые экземпляры.
     *		Необходим вследствие неочевидности в возможности создания экземпляров класса
     *		другими способами (ведь нужно использовать метод readFromFile)
     * @throws IOException невозможно работать с файлом
     * @throws IllegalStateException если файловая структура не создана
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @return LinkedList, содержащий все прочитанные данные
     */
    public LinkedList<E> removeAllFor(int Index, E temp)
	    throws IOException, InstantiationException, IllegalAccessException,
	    IllegalStateException
    {
	if (MainFile == null || DataFile == null)
	    throw new IllegalStateException();
	if (FilePointers[Index] == 0) return null;
	
	LinkedList<E> RetList = new LinkedList<E>();
	MainFile.seek(FilePointers[Index] + LONGSIZE);	//Начинаем с первого блока
	long NextBlock = -1;
	do  //Пока не дойдем до конца
	{
	    long BlockCap = MainFile.readLong();    //Читаем размер блока
	    while (BlockCap != 0)   //Пока в блоке есть данные
	    {
		long DataRef = MainFile.readLong(); //Получаем ссылку на данные
		DataFile.seek(DataRef);
		E some = (E)temp.getClass().newInstance();
		some.readFromFile(DataFile);	    //Считываем данные
		RetList.add(some);		    //и добавляем в список
		BlockCap--;	
	    }
	    NextBlock = MainFile.readLong();
	    if (NextBlock != -1 && NextBlock != 0) MainFile.seek(NextBlock);//Переход к следующему блоку
	} while (NextBlock != -1 && NextBlock != 0);
	
	//После того, как все данные изъяты, нужно убрать ссылки на них
	MainFile.seek(FPOFF + Index*LONGSIZE);
	MainFile.writeLong(0);
	FilePointers[Index] = 0;
	IndexesAmount--;
	//Если больше не осталось данных ни для каких индексов - пересоздаем файлы
	if (IndexesAmount == 0)
	{
	    MainFile.close();
	    DataFile.close();
	    createDataStorage(MF.getPath(), DF.getPath(), N, M);
	}
	return RetList;
    }
    
    /** Закрытие файлов и очистка всей связанной информации */
    public void closeStorage() throws IOException
    {
	MainFile.close(); DataFile.close();
	MainFile = null; DataFile = null;
	FilePointers = null; MF = DF = null;
	N = M = IndexesAmount = 0;
	FPOFF = 0;
    }
    
    /** Загрузка файловой структуры из указанного главного файла<br>
     * Если загрузка вызвана при загруженной ранее структуре, производится попытка
     * безболезненно закрыть все файлы и очистить все данные
     * @throws IOException could not work with file
     * @param MFName имя и путь к главному файлу описания структуры
     */
    public void loadStorage(String MFName) throws IOException, FileNotFoundException
    {
	//Закрываем все файлы
	if (MainFile != null) closeStorage();
	
	MF = new File(MFName);
	try
	{
	    //Есть ли файл
	    if (!MF.exists()) throw new FileNotFoundException("MainFile " + MFName);
	    MainFile = new RandomAccessFile(MF, "rw");
	    //Считываем описание структуры
	    N = MainFile.readInt();
	    M = MainFile.readInt();
	    int strsz = MainFile.readInt();
	    byte[] b = new byte[strsz];
	    MainFile.read(b);
	    String DFName = new String(b);
	    DF = new File(DFName);
	    //Ищем файл данных
	    if (!DF.exists()) throw new FileNotFoundException("DataFile " + DFName);
	    //Инициализируем данные
	    FilePointers = new long[N];
	    FPOFF = 3*INTSIZE + strsz;
	    IndexesAmount = 0;
	    //Считываем файловые указатели на блоки
	    for (int i = 0; i < N; i++)
	    {
		FilePointers[i] = MainFile.readLong();
	    }
	    DataFile = new RandomAccessFile(DF, "rw");  
	} catch (IOException e)
	{
	    closeStorage();
	    throw e;
	}
	//Производим подсчет связанных индексов
	for (int i = 0; i < N; i++)
	{
	    if (FilePointers[i] != 0) IndexesAmount++;
	}
    }
}


















